package preparation;

import java.util.ArrayList;
import java.util.List;

import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.Rect;

import com.google.zxing.BinaryBitmap;
import com.google.zxing.client.android.PlanarYUVLuminanceSource;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;

/**
 * 
 * PrepareMatrix sluzi prepoznavanju 29x29 QR kod koji treba dalje da se salje na dekodiranje
 * 
 * @author Vojislav Djukic Nikola Luburic
 *
 */
public class PrepareMatrix {
	
	/**
	 * Priprema ulaznu sliku za dekodiranje
	 * @param source 
	 * 		slika sa kamere u obliku PlanarYUVLuminanceSource na kojoj je potencijalni QR kod
	 * @return 
	 * 		29x29 matricu kao binarnu sliku ili null ako QR kod nije prepoznat
	 * */
	public static BinaryBitmap prepareForRead(PlanarYUVLuminanceSource source) {
		//pretvori ulaz u matricu
		short[][] matrix = createMatrix(source);
		//pretvori u binarnu sliku
		matrix = matrixToBinaryTiles(matrix, 15, 15);
		//pretvori u binarnu matricu 29x29
		short[][] qrMatrix = getQRmatrix(matrix);
		
		//konvertuj sliku u adekvatni izlazni format
		if (qrMatrix != null) {
			BitMatrix bm = new BitMatrix(29, 29);
			byte[] data = new byte[841];
			for (int i = 0; i < 29; i++) {
				for (int j = 0; j < 29; j++) {
					if (qrMatrix[i][j] == 255) {
						bm.set(i, j);
					}
					data[i * 29 + j] = (byte) qrMatrix[i][j];
				}
			}
			source = new PlanarYUVLuminanceSource(data, 29, 29, 0, 0, 29, 29, false);
		}
		BinaryBitmap bmp = new BinaryBitmap(new HybridBinarizer(source));
		return bmp;
	}

	/**
	 * Prevodjenje slike iz YUV modela u matricu koja ima vrednosti od 0 do 255 koje repreztuju sivu boju
	 * @param source
	 * 		izvorna slika u YUV formatu
	 * @return	
	 * 		matrica nijansi sive
	 * */
	protected static short[][] createMatrix(PlanarYUVLuminanceSource source) {
		Bitmap image = source.renderCroppedGreyscaleBitmap();

		final int height = image.getHeight();
		final int width = image.getWidth();
		
		//pretvori sliku u nijanse sive
		final Bitmap bmpGrayscale = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
		final Canvas c = new Canvas(bmpGrayscale);
		final Paint paint = new Paint();
		final ColorMatrix cm = new ColorMatrix();
		cm.setSaturation(0);
		final ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
		paint.setColorFilter(f);
		c.drawBitmap(image, 0, 0, paint);

		int h = bmpGrayscale.getHeight();
		int w = bmpGrayscale.getWidth();
		
		//pretvori nijanse sive u matricu
		short[][] matrix = new short[h][];
		for (int i = 0; i < h; i++) {
			matrix[i] = new short[w];
		}

		for (int i = 0; i < h; i++) {
			for (int j = 0; j < w; j++) {
				matrix[i][j] = (short) (Color.red(bmpGrayscale.getPixel(j, i)) + 128);
			}
		}

		return matrix;
	}

	/**
	 * Metoda za menjanje veličine slike.
	 * 
	 * @param src
	 *            izvorna slika u matričnom obliku.
	 * @param height
	 *            visina nove slike.
	 * @param width
	 *            širina nove slike.
	 * @return matricu height x width koja predstavlja resizovanu sliku.
	 */
	public static short[][] resizeImage(short[][] src, int height, int width) {
		int sourceWidth = src.length;
		int sourceHeight = src[0].length;

		short[][] resizedMatrix = new short[height][width];

		int stepHeight = (int) (sourceHeight / height + 0.5);
		int stepWidth = (int) (sourceWidth / width + 0.5);

		for (int i = 0; i < sourceHeight; i += stepHeight) {
			for (int j = 0; j < sourceWidth; j += stepWidth) {
				int brojac = 0;
				for (int ii = i; ii < stepHeight; ii++) {
					for (int jj = j; jj < stepWidth; j++) {
						if (src[ii][jj] == 255) {
							brojac++;
						} else {
							brojac--;
						}
					}
				}
				if (brojac > 0) {
					resizedMatrix[i / stepHeight][j / stepWidth] = 255;
				} else {
					resizedMatrix[i / stepHeight][j / stepWidth] = 0;
				}
			}
		}
		return resizedMatrix;
	}

	/**
	 * Pretvaranje matrice nijansi sive u binarnu sliku putem podele na regione. Threshold se radi na nivou
	 * pojedinacnog regiona
	 * @param slika
	 * 		nijanse sive matrica
	 * @return
	 * 		binarna matrica (sa vrednostima ili 0 ili 255)
	 * */
	protected static short[][] matrixToBinaryTiles(short[][] slika, int R, int C) {
		int h = slika.length;
		int w = slika[0].length;
		double dW = (double) w / C;
		double dH = (double) h / R;
		double[][] means = new double[R][C];
		short[][] retVal = new short[h][w];
		int D = 4;
		for (int r = 0; r < R; r++) {
			for (int c = 0; c < C; c++) {
				means[r][c] = 0;
				int minD = 0;
				int maxD = 0;
				int maxDif = 0;
				for (int y = 0; y < dH; y++) {
					int A = 0;
					int B = 0;
					for (int x = 0; x < D; x++) {
						A += slika[(int) (r * dH) + y][(int) (c * dW) + x];
					}
					for (int x = D; x < 2 * D; x++) {
						B += slika[(int) (r * dH) + y][(int) (c * dW) + x];
					}
					for (int x = D; x < dW - D; x++) {
						int diff = Math.abs(A - B);
						if (diff >= maxDif) {
							maxDif = diff;
							minD = Math.min(A, B);
							maxD = Math.max(A, B);
						}
						A -= slika[(int) (r * dH) + y][(int) (c * dW) + x - D];
						A += slika[(int) (r * dH) + y][(int) (c * dW) + x];
						B -= slika[(int) (r * dH) + y][(int) (c * dW) + x];
						B += slika[(int) (r * dH) + y][(int) (c * dW) + x + D - 1];
					}
				}
				int TT = (maxD + minD) / (2 * D);
				int DD = (maxD - minD) / D;
				for (int y = 0; y < dH; y++) {
					for (int x = 0; x < dW; x++) {
						if (DD > 20) {
							if (slika[(int) (r * dH) + y][(int) (c * dW) + x] < TT)// means[r, c])
								retVal[(int) (r * dH) + y][(int) (c * dW) + x] = 0;
							else
								retVal[(int) (r * dH) + y][(int) (c * dW) + x] = (short) 255;
						} else {
							if (TT < 80)
								retVal[(int) (r * dH) + y][(int) (c * dW) + x] = 0;
							else
								retVal[(int) (r * dH) + y][(int) (c * dW) + x] = (short) 255;
						}
					}
				}
			}
		}
		return retVal;
	}

	/**
	 * Pronalazenje QR koda u binarnoj matrici i konvertovanje matrice u 29x29 binarnu matricu
	 * @param matrix
	 * 		binarna slika
	 * @return
	 * 		prepoznata matrica 29x29 ili null ako nije prepoznat QR kod
	 * */
	private static short[][] getQRmatrix(short[][] matrix) {
		final int xDistanceConst = 5;
		final int yDistanceConst = 5;

		//obelezi regione na slici
		List<RasterRegion> regions = RasterRegion.regionLabeling(matrix, 1);
		for (RasterRegion reg : regions) {
			reg.odrediMomente();
		}
		List<RasterRegion> regionPair = new ArrayList<RasterRegion>();

		//trazi parove regiona ciji se centri poklapaju
		RasterRegion reg1;
		RasterRegion reg2;
		for (int i = 0; i < regions.size() - 1; i++) {
			reg1 = regions.get(i);
			for (int j = i + 1; j < regions.size(); j++) {
				reg2 = regions.get(j);
				if (Math.abs(reg1.xM - reg2.xM) < xDistanceConst && Math.abs(reg1.yM - reg2.yM) < yDistanceConst) {
					regionPair.add(reg1);
					regionPair.add(reg2);
					break;
				}
			}
		}

		//mora biti pronadjeno tacno 6 regiona (3 para). Ovih 6 regiona je 6 kvadrata na koji sluze
		//za pozicioniranje QR koda na slikama
		if (regionPair.size() != 6) {
			return null;
		}

		//spojiti regione za pozicioniranje (sada se dobijaju 3 regiona)
		for (int i = 0; i < regionPair.size(); i++) {
			regionPair.get(i).points.addAll(regionPair.get(i + 1).points);
			regionPair.get(i).odrediMomente();
			regionPair.remove(i + 1);
		}

		//radimo ponovo obelezavanje regiona ali sa faktorom 6 (6 piksela moze biti razmak izmedju 
		//dva obojena piksela, a da oni i dalje budu bili jedan region)
		//Ovde se ocekuje da ce ceo QR kod biti jedan region
		regions = RasterRegion.regionLabeling(matrix, 6);
		RasterRegion QRRegion = null;
		boolean foundQRRegion = true;
		//proverava se da li neki od novih regiona sadrzi prethodno detektovane kvadrate
		for (RasterRegion reg : regions) {
			reg.odrediMomente();
			Rect boundary = new Rect(reg.minX, reg.maxY, reg.maxX, reg.minY);
			for (RasterRegion regPair : regionPair) {
				if (!boundary.contains((int) (regPair.xM + 0.5), (int) (regPair.yM + 0.5))) {
					foundQRRegion = false;
					break;
				}
			}
			if (foundQRRegion) {
				QRRegion = reg;
				break;
			}
		}

		short[][] QRMatrix = null;

		//ako je pronadjen region zarotirati ga i promeniti velicinu matrici na 29x29 
		if (QRRegion != null) {
			QRMatrix = resizeImage(QRRegion.odrediNSliku(), 29, 29);
		}

		return QRMatrix;
	}
}
